using System;
using System.Globalization;
using System.IO;
using System.Reflection;

namespace Drill.Core.Utilities
{
    /// <summary>
    /// Provides Assembly utility methods.
    /// </summary>
    public static class AssemblyUtility
    {
        /// <summary>
        /// Creates an instance of the type specified as an assembly-qualified type name.
        /// </summary>
        /// <typeparam name="T">The type to be created.</typeparam>
        /// <param name="assemblyQualifiedTypeName">The assembly-qualified type name.</param>
        /// <param name="typeLabel">The type label.</param>
        /// <param name="constructorArgs">An array of objects representing the constructor arguments.</param>
        /// <returns>
        /// An instance of the specified type or null if the instance could not be created.
        /// </returns>
        internal static T CreateInstance<T>(string assemblyQualifiedTypeName, string typeLabel, object[] constructorArgs)
            where T : class
        {
            Assembly assembly;
            Type type;

            if (TryLoadAssemblyQualifiedTypeName(assemblyQualifiedTypeName, out assembly, out type))
            {
	            return CreateInstance<T>(type, typeLabel, constructorArgs);
            }

            throw new DependencyResolverException(
                string.Format("The {0} type or its containing assembly '{1}' could not be loaded.",
                typeLabel,
                assemblyQualifiedTypeName));
        }

		internal static T CreateInstance<T>(Type type, string typeLabel, object[] constructorArgs)
			where T : class
		{
			if (false == typeof(T).IsAssignableFrom(type))
			{
				throw new DependencyResolverException(
					string.Format("The {0} type value '{1}' does not implement the interface '{2}'."
					, typeLabel
					, type.AssemblyQualifiedName
					, typeof(T).FullName
					));
			}

			try
			{
				T obj = Activator.CreateInstance(
									  type                          // type to create
									, BindingFlags.CreateInstance   // binding flags
									, null                          // binder
									, constructorArgs               // constructor args
									, CultureInfo.CurrentCulture    // culture
									  ) as T;

				return obj;
			}
			catch (Exception ex)
			{
				if (ex is DependencyResolverException)
				{
					throw;
				}

				// ReSharper disable RedundantComparisonWithNull
				if (ex is TargetInvocationException && null != ex.InnerException && ex.InnerException is DependencyResolverException)
				{
					throw ex.InnerException;
				}
				// ReSharper restore RedundantComparisonWithNull

				throw new DependencyResolverException(
					ex,
					"The {0} type '{1}' could not be created. {2}",
					typeLabel,
					type.AssemblyQualifiedName,
					ex.Message
					);
			}
		}

        /// <summary>
        /// Ensures that the specified assembly is loaded in the current AppDomain.
        /// </summary>
        /// <param name="assemblyName">An instance of the <see cref="AssemblyName" /> class representing the assembly to find or load.</param>
        /// <returns>A reference to the loaded assembly or null if the assembly could not be loaded.</returns>
        public static Assembly EnsureAssemblyIsLoaded(AssemblyName assemblyName)
        {
            return FindLoadedAssembly(assemblyName, AppDomain.CurrentDomain) ?? Assembly.Load(assemblyName);
        }

        #region " Removed Unused Code "

        ///// <summary>
        ///// Ensures that the specified assembly is loaded in the current AppDomain.
        ///// </summary>
        ///// <param name="name">The name of the assembly.</param>
        ///// <returns>A reference to the loaded assembly or null if the assembly could not be loaded.</returns>
        //public static Assembly EnsureAssemblyIsLoaded(string name)
        //{
        //    return FindLoadedAssembly(name) ?? Assembly.Load(name);
        //}

        ///// <summary>
        ///// Finds a loaded assembly in the current AppDomain.
        ///// </summary>
        ///// <param name="name">The name of the assembly to find.</param>
        ///// <returns>A reference to the Assembly if it is found, otherwise null.</returns>
        //public static Assembly FindLoadedAssembly(string name)
        //{
        //    return FindLoadedAssembly(name, AppDomain.CurrentDomain);
        //}

        ///// <summary>
        ///// Finds a loaded assembly in the specified AppDomain.
        ///// </summary>
        ///// <param name="name">The name of the assembly to find.</param>
        ///// <param name="domain">The AppDomain to be searched.</param>
        ///// <returns>A reference to the Assembly if it is found, otherwise null.</returns>
        //public static Assembly FindLoadedAssembly(string name, AppDomain domain)
        //{
        //    ArgumentException argumentException =
        //        ValidationUtility.GetArgumentExceptionForStringIsNullOrIsEmptyOrContainsOnlyWhitespace(name, "name");
        //    if (null != argumentException)
        //    {
        //        throw argumentException;
        //    }

        //    if (null == domain)
        //    {
        //        throw new ArgumentNullException("domain");
        //    }

        //    AssemblyName assemblyName;

        //    try
        //    {
        //        assemblyName = new AssemblyName(name);
        //    }
        //    catch
        //    {
        //        throw new ArgumentException("The name parameter is not a valid assembly name.");
        //    }

        //    return FindLoadedAssembly(assemblyName, domain);
        //}

        #endregion " Removed Unused Code "

        /// <summary>
        /// Finds a loaded assembly in the specified AppDomain.
        /// </summary>
        /// <param name="assemblyName">An instance of the <see cref="AssemblyName" /> class representing the assembly to find.</param>
        /// <param name="domain">The AppDomain to be searched.</param>
        /// <returns>A reference to the Assembly if it is found, otherwise null.</returns>
        public static Assembly FindLoadedAssembly(AssemblyName assemblyName, AppDomain domain)
        {
            if (null == assemblyName)
            {
                throw new ArgumentNullException("assemblyName");
            }

            if (null == domain)
            {
                throw new ArgumentNullException("domain");
            }

            Assembly[] assemblies = domain.GetAssemblies();

            if (assemblies.Length > 0)
            {
                foreach (Assembly assembly in assemblies)
                {
                    if (0 == String.Compare(assembly.FullName, assemblyName.FullName, StringComparison.OrdinalIgnoreCase) ||
                        0 == String.Compare(assembly.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        return assembly;
                    }
                }
            }

            return null;
        }

        #region " Removed Unused Code "

        ///// <summary>
        ///// Gets directory in which the specified assembly resides.
        ///// </summary>
        ///// <param name="assembly">The assembly.</param>
        ///// <returns>The absolute path to the directory that contains the assembly's file or null if the directory cannot be obtained.</returns>
        //public static string GetAssemblyDirectory(Assembly assembly)
        //{
        //    if (null != assembly)
        //    {
        //        string path = assembly.Location;

        //        string dir = Path.GetDirectoryName(path);

        //        return dir;
        //    }
        //    return null;
        //}

        ///// <summary>
        ///// Loads an assembly from an absolute path and file name..
        ///// </summary>
        ///// <param name="absoluteFileName">The absolute path and file name of the assembly to be loaded.</param>
        ///// <returns>A reference to the loaded assembly or null if the assembly could not be loaded.</returns>
        //public static Assembly LoadAssemblyFromAbsolutePathAndFileName(string absoluteFileName)
        //{
        //    // Ensure that the file exists.
        //    if (false == File.Exists(absoluteFileName))
        //    {
        //        return null;
        //    }

        //    // Before attempting to load the assembly from the file, 
        //    // determine if the file has an assembly manifest by 
        //    // trying to load the assembly name from the file.
        //    // This check ensures that the file is an assembly that 
        //    // is capable of being loaded.
        //    AssemblyName assemblyName;

        //    try
        //    {
        //        assemblyName = AssemblyName.GetAssemblyName(absoluteFileName);
        //    }
        //    catch
        //    {
        //        assemblyName = null;
        //    }

        //    if (null == assemblyName)
        //    {
        //        return null;
        //    }

        //    // Attempt to load the assembly into the current AppDomain.
        //    Assembly assembly;

        //    try
        //    {
        //        assembly = Assembly.LoadFrom(absoluteFileName);
        //    }
        //    catch
        //    {
        //        assembly = null;
        //    }

        //    if (null == assembly)
        //    {
        //        // Try alternative method.
        //        try
        //        {
        //            assembly = Assembly.LoadFile(absoluteFileName);
        //        }
        //        catch
        //        {
        //            assembly = null;
        //        }
        //    }

        //    return assembly;
        //}

        #endregion " Removed Unused Code "

        /// <summary>
        /// Splits an assembly-qualified type name into two parts: assembly name and type name.
        /// </summary>
        /// <param name="assemblyQualifiedTypeName">The assembly-qualified type name that will be split.</param>
        /// <param name="assemblyName">Outputs the assembly name portion of the assembly-qualified type name.</param>
        /// <param name="typeName">Outputs the type name portion of the assembly-qualified type name.</param>
        /// <returns><c>true</c> if the assemblyQualifiedTypeName parameter was split successfully, otherwise <c>false</c>.</returns>
        public static bool TrySplitAssemblyQualifiedTypeName(string assemblyQualifiedTypeName, out string assemblyName, out string typeName)
        {
            if (string.IsNullOrWhiteSpace(assemblyQualifiedTypeName))
            {
                throw new ArgumentNullException("assemblyQualifiedTypeName");
            }

            int i = assemblyQualifiedTypeName.IndexOf(',');

            if (i > -1)
            {
                typeName= assemblyQualifiedTypeName.Substring(0, i).Trim();

                assemblyName = assemblyQualifiedTypeName.Substring(++i).Trim();

                if (false == string.IsNullOrEmpty(typeName) && 
                    false == string.IsNullOrEmpty(assemblyName))
                {
                    return true;
                }
            }

            assemblyName = null;
            
            typeName = null;

            return false;
        }

        /// <summary>
        /// Splits an assembly-qualified type name into two parts: assembly name and type name.
        /// </summary>
        /// <param name="assemblyQualifiedTypeName">The assembly-qualified type name that will be split.</param>
        /// <param name="assemblyName">Outputs the assembly name portion of the assembly-qualified type name.</param>
        /// <param name="typeName">Outputs the type name portion of the assembly-qualified type name.</param>
        /// <returns><c>true</c> if the assemblyQualifiedTypeName parameter was split successfully, otherwise <c>false</c>.</returns>
        public static bool TrySplitAssemblyQualifiedTypeName(string assemblyQualifiedTypeName, out AssemblyName assemblyName, out string typeName)
        {
			if (string.IsNullOrWhiteSpace(assemblyQualifiedTypeName))
			{
				throw new ArgumentNullException("assemblyQualifiedTypeName");
			}

            string assemblyNameString;
            string typeNameString;

            if (TrySplitAssemblyQualifiedTypeName(assemblyQualifiedTypeName, out assemblyNameString, out typeNameString))
            {
                AssemblyName an;

                try
                {
                    an = new AssemblyName(assemblyNameString);
                }
                catch
                {
                    an = null;
                }

                if (null != an)
                {
                    assemblyName = an;
                    typeName = typeNameString;
                    return true;
                }
            }

            assemblyName = null;

            typeName = null;

            return false;
        }

        /// <summary>
        /// Loads the assembly and type from an assembly-qualified type name.
        /// </summary>
        /// <param name="assemblyQualifiedTypeName">The assembly-qualified type name.</param>
        /// <param name="assembly">Outputs the assembly.</param>
        /// <param name="type">Outputs the type.</param>
        /// <returns><c>true</c> if the assembly and type were loaded successfully, otherwise <c>false</c>.</returns>
        public static bool TryLoadAssemblyQualifiedTypeName(string assemblyQualifiedTypeName, out Assembly assembly, out Type type)
        {
			if (string.IsNullOrWhiteSpace(assemblyQualifiedTypeName))
			{
				throw new ArgumentNullException("assemblyQualifiedTypeName");
			}

            AssemblyName assemblyName;
            string typeName;

            if (TrySplitAssemblyQualifiedTypeName(assemblyQualifiedTypeName, out assemblyName, out typeName))
            {
                try
                {
                    assembly = EnsureAssemblyIsLoaded(assemblyName);
                }
                catch (FileNotFoundException)
                {
                    assembly = null;
                    type = null;
                    return false;
                }
                
                if (null != assembly)
                {
                    type = assembly.GetType(typeName, false, true);

                    if (null != type)
                    {
                        return true;
                    }
                }
            }

            assembly = null;
            type = null;
            return false;
        }
    }
}